package com.dman.multi.core;

import java.io.EOFException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SelectionKey;
import java.nio.channels.UnresolvedAddressException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import com.dman.multi.algo.SectionAlgo;
import com.dman.multi.algo.SectionAlgoFactory;
import com.dman.multi.algo.SectionCreationException;
import com.dman.multi.proxy.ProxyAdaptor;
import com.dman.multi.ui.Settings;

public class Download implements Serializable, Downloadable, Displayable {

	/**
	 * The serial version UID of the serializable
	 */
	private static final long serialVersionUID = 1L;
	
	// constructor arguments
	private String url;
	private long startTime = 0;
	private String dirName;
	private String fileName;

	// the misc variables that would be persisted
	private Section firstSection = null;
	private List<Section> secList;
	private long bytesProcessed = 0;
	private long size = 0;
	private boolean rangeSupported;
	private String algo;
	private FileStream fileStream;
	private DownloadState downloadState;
	private long endTime;
		
	// define the transients
	private transient long sessionBytesProcessed = 0;
	private transient long readBytesProcessed = 0;
	private transient Map<Section, Long> secLimitLookup = new HashMap<Section, Long>();
	private transient NetworkStream netStream;
	private transient SectionAlgo secAlgo;
	private transient ScheduledFuture<?> schedFuture = null;
	private transient double downloadSpeed;
	private transient int numTimes = 0;
	private transient boolean visible = true;
	private transient ProxyAdaptor proxyAuth = null;

	// define the static variables
	public static final int MAX_THREADS = 12;
	public static final String CONFIG_DIR = ".javadm";
	public static final String FULL_CONF_DIR = System.getProperty("user.home", "~") + File.separator + CONFIG_DIR;
	private static final long SOCK_TIMEOUT = 30000;
	private static final long WAIT_TIME = 10000;
	private static final int MAX_TIMES = 6;
	
	public static ScheduledThreadPoolExecutor schedExec = new ScheduledThreadPoolExecutor(MAX_THREADS);
	private static Thread settingsThread = null;
	private static DownloadSettings downloadSettings = null;
		
	/**
	 * The download constructors.
	 * @throws IOException 
	 */
	public Download(String dirName, String url) throws IOException{
		this(dirName, url, null);
	}
	
	public Download(String dirName, String url, String fileName) throws IOException{
		this(dirName, url, fileName, Calendar.getInstance().getTimeInMillis());
	}

	public Download(String dirName, String url, String fileName, long startTime) throws IOException {
		if(fileName == null)
			fileName = FileStream.getFileName(new URL(url));
		this.url = url;
		this.startTime = startTime;
		this.dirName = dirName;
		this.fileName = fileName;
		fileStream = new FileStream(this.dirName, this.fileName);
		secList = new LinkedList<Section>();
		downloadState = DownloadState.NEW;
		init();
	}
	
	private void init() throws IOException{

		secLimitLookup = new HashMap<Section, Long>();
		fileStream.createFile();
		if(downloadSettings == null)
			downloadSettings = Settings.getSettings();	
			
		synchronized (downloadSettings) {
			proxyAuth = ProxyAdaptor.NO_PROXYAUTH;
			if(downloadSettings != null){
				ProxySettings ps = downloadSettings.getProxySettings();
				if(ps != null){
					proxyAuth = ps.getProxyAuth();
				}
			}
			
			netStream = new ThrottledNetworkStream(downloadSettings.getConnSpeed());
			schedExec.setCorePoolSize(downloadSettings.getMaxConcurrentDownloads());

			/*if(semaphore == null)
				semaphore = new Semaphore(downloadSettings.getMaxConcurrentDownloads());*/
		}
		
		listenSettingsChanges();
	}
	
	private static void listenSettingsChanges(){
		if(settingsThread == null){
			settingsThread = new Thread(new Runnable() {				
				@Override
				public void run() {
					while(true){
						synchronized (downloadSettings) {
							try {
								downloadSettings.wait();
								int currMaxConcurrent = downloadSettings.getMaxConcurrentDownloads();
								schedExec.setCorePoolSize(currMaxConcurrent);
							} catch (InterruptedException e) {
								// TODO Auto-generated catch block
								e.printStackTrace();
							}
						}
					}
				}
			});
			settingsThread.setDaemon(true);
			settingsThread.start();
		}
	}
	
	private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
		ois.defaultReadObject();
		init();
	}

	public boolean isVisible() {
		return visible;
	}
	
	public void setVisible(boolean visible){
		this.visible = visible;
	}
	
	private void startDownload() throws IOException {
		
		// update the status of download
		downloadState = DownloadState.RUNNING;
		
		// check if first section is null, if so create one (o/w it means we're a serialized instance)
		boolean isFirst = false;
		if(firstSection == null){
			firstSection = new Section(url, 0, proxyAuth);
			secList.add(firstSection);
			isFirst = true;
		}
		
		// now register the sections and find size
		for(Section sec: secList){
			if(!sec.isClosed()){
				netStream.registerSection(sec);
			}
		}
		Long lng = firstSection.getSize();
		
		// get the size and section algorithm
		size = -1;
		algo = "Dummy";
		SectionAlgoFactory secAlgoFactory = SectionAlgoFactory.getInstance();
		if(lng != null){
			size = lng;
			rangeSupported = size >= 0;
			size = -size;
			if(rangeSupported){
				size = -size;
				algo = "Balanced";
			}
		}
		secAlgo = secAlgoFactory.getSectionAlgo(algo);
		
		// reserve space and build section to limit lookup
		if(isFirst) 
			fileStream.reserveSpace(size);
		buildSecLimitLookup();
	}
	
	private int createSectionsIfPossible(int index) throws IOException{
		
		// if range is supported try creating another section
		if(index >= 0 && rangeSupported){
			try{
				index = secAlgo.createSection(secList);
				
				// if creation succeeded, check if the server supports so many sockets
				if(index > 0){
					Section sec = secList.get(index);
					buildSecLimitLookup();
					netStream.registerSection(sec);
				}
			} catch (SectionCreationException e){
				// if unable to create a section
				index = -1;
			} catch (ConnectException e){
				// if server does not support the specified number of sockets, remove the section
				synchronized(secList){
					secList.remove(index);
				}
				buildSecLimitLookup();
				index = -1;
			}
		}
		
		return index;
	}
	
	private Set<SelectionKey> readySockets() throws IOException, InterruptedException{
		
		// try to read from the sockets pending
		Set<SelectionKey> sockSet = netStream.getReadySockets(SOCK_TIMEOUT);
		boolean sectionPending = false;
		
		/* 
		 * if there are no sockets ready, iterate and find if there 
		 * are open sections, if there are open sections, unregister
		 * them from selectors and close them and re-init them.
		 */
		if(sockSet.isEmpty()){
			boolean interrupted = Thread.interrupted();
			if(interrupted)
				throw new InterruptedException("An interrupt occurred while selecting ready sockets");
			for(Section sec: secList){
				if(!sec.isClosed()){
					// re-init the section which hung
					netStream.reinit(sec);
					sectionPending = true;
				}
			}
			
			// if there are one or more sections pending, read again
			if(sectionPending)
				sockSet = netStream.getReadySockets(SOCK_TIMEOUT);
		}
		
		return sockSet;
	}

	@Override
	public void run() {

		try {
			
			// create the first section and register with network stream
			startDownload();			
			
			// run through an endless loop
			ByteBuffer bb = null;
			Set<SelectionKey> sockSet = null;
			Section sec = null;
			int index = 0;
			boolean interrupt = false;
			Exception ex = null;
			boolean endOfSecReached = false;
			
			// variable(s) for measuring speed
			DownloadSpeed ds = new DownloadSpeed();
			while(true){
				// reset read bytes processed and startTime every 2 minutes
				downloadSpeed = ds.measureSpeed();
				readBytesProcessed = 0;
				
				// get ready sockets, if interrupted while waiting create an empty set
				try{
					sockSet = readySockets();
				
					// read the ready sockets
					for(SelectionKey selKey: sockSet){
						// read from the socket
						try{
							bb = netStream.readSocket(selKey);
						} catch(EOFException ee){
							endOfSecReached = true;
						}
						sec = (Section) selKey.attachment();
						
						// write to file
						long numBytes = fileStream.writeSection(sec, bb);
						bytesProcessed += numBytes;
						sessionBytesProcessed += numBytes;
						readBytesProcessed += numBytes;
						
						// check for completion
						long limit = secLimitLookup.get(sec);
						if(size >= 0 && sec.getCurrPos() >= limit){
							bytesProcessed -= sec.getCurrPos() - limit - 1;
							sessionBytesProcessed -= sec.getCurrPos() - limit - 1;
							readBytesProcessed -= sec.getCurrPos() - limit - 1;
							endOfSecReached = true;
						}
						
						// perform the necessary clean-up
						if(endOfSecReached){
							sec.close();
							selKey.cancel();
							fileStream.flush();
							index = 0;
							endOfSecReached = false;
						}
					}

					// clear the set of sockets
					sockSet.clear();
					
					// populate download speed
					ds.addData(readBytesProcessed);
					
					// check for stopping condition
					boolean downloadComplete = isDownloadComplete();
					if(downloadComplete || Thread.interrupted()){
						interrupt = true;
						if(downloadComplete){
							downloadState = DownloadState.COMPLETE;
							endTime = Calendar.getInstance().getTimeInMillis();
						}
						index = -1;
					}
					
					// create additional sections if possible
					index = createSectionsIfPossible(index);
				} catch (ClosedByInterruptException cbie){
					interrupt = true;
				} catch (InterruptedException ie){
					interrupt = true;
				} catch (UnresolvedAddressException uae){
					ex = uae;
					if(numTimes > MAX_TIMES){
						interrupt = true;
						ex = null;
						downloadState = DownloadState.PAUSED;
					}
				}
				
				if(interrupt || ex != null){
					fileStream.flush();
					if(ex != null) throw ex;
					netStream.close();
					fileStream.close();
					break;
				}
			}
			
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
			downloadState = DownloadState.PAUSED;
			if(numTimes++ <= MAX_TIMES){
				schedFuture = null;
				startTime += WAIT_TIME * numTimes;
				schedule();
			}
		} catch (Throwable t){
			t.printStackTrace();
		}
	}
	
	public boolean isDownloadComplete() {
		boolean isComplete = true;
		for(Section sec: secList){
			isComplete &= sec.isClosed();
		}
		
		return isComplete;
	}

	public void buildSecLimitLookup(){
		Section prevSec = null;
		for(Section sec: secList){
			if(prevSec != null){
				secLimitLookup.put(prevSec, sec.getStartPos() - 1);
			}
			prevSec = sec;
		}
		secLimitLookup.put(prevSec, size - 1);
	}

	@Override
	public void schedule() {
		if(schedFuture == null){
			long currTime = Calendar.getInstance().getTimeInMillis();
			long delay = 0;
			if(startTime > currTime)
				delay = startTime - currTime;
			schedFuture = schedExec.schedule(this, delay, TimeUnit.MILLISECONDS);
			downloadState = DownloadState.SCHEDULED;
		}
	}

	@Override
	public void pause(boolean save) throws IOException, UnsupportedOperationException {
		if(!isResumeSupported())
			throw new UnsupportedOperationException("The download cannot be stopped in the middle.");
		if(!isDownloadComplete()){
			schedFuture.cancel(true);
			downloadState = DownloadState.PAUSED;
		}
		while(!schedFuture.isCancelled());
		if(save) saveState();
	}

	@Override
	public void pause(final boolean save, long stopTime) throws IOException, UnsupportedOperationException {
		if(!isResumeSupported())
			throw new UnsupportedOperationException("The download cannot be stopped in the middle.");
		Callable<Void> c = new Callable<Void>() {
			@Override
			public Void call() throws Exception {
				if(!isDownloadComplete()){
					schedFuture.cancel(true);
					downloadState = DownloadState.PAUSED;
				}
				while(!schedFuture.isCancelled());
				if(save) saveState();
				
				return null;
			}
		};
		long currTime = Calendar.getInstance().getTimeInMillis();
		schedExec.schedule(c, stopTime - currTime, TimeUnit.MILLISECONDS);
	}
	
	@Override
	public void saveState() throws IOException{
		String fileName = FULL_CONF_DIR;
		File f = new File(FULL_CONF_DIR);
		if(!f.exists())
			f.mkdirs();
		fileName += File.separator + fileStream.getFileName() + ".ser";
		FileOutputStream fos = new FileOutputStream(fileName);
		ObjectOutputStream oos = new ObjectOutputStream(fos);
		oos.writeObject(this);
	}

	@Override
	public void resume() throws UnsupportedOperationException, IOException{
		if(!isResumeSupported())
			throw new UnsupportedOperationException("The download cannot be stopped in the middle.");
		if((schedFuture == null || schedFuture.isDone()) && !isDownloadComplete()){
			init();
			long currTime = Calendar.getInstance().getTimeInMillis();
			schedFuture = schedExec.schedule(this, startTime - currTime, TimeUnit.MILLISECONDS);
		}
	}

	@Override
	public void delete() throws UnsupportedOperationException, IOException {
		if(!isDownloadComplete())
			pause(false);
		String fileName = FULL_CONF_DIR + File.separator + fileStream.getFileName() + ".ser";
		File f = new File(fileName);
		if(f.exists())
			f.delete();
		schedFuture = null;
	}

	@Override
	public void restart() throws UnsupportedOperationException, IOException {
		delete();
		synchronized (secList) {
			secList.clear();
			firstSection = null;
			bytesProcessed = 0;
			sessionBytesProcessed = 0;
		}
		init();
		schedule();
	}

	@Override
	public boolean isResumeSupported() {
		return rangeSupported;
	}
	
	@Override
	public DownloadState getDownloadState() {
		return downloadState;
	}

	@Override
	public String getFileName() {
		return fileStream.getFileName();
	}

	@Override
	public long getSize() {
		return size;
	}

	@Override
	public double getPercentComplete() {
		return bytesProcessed * 100.0 / size;
	}

	@Override
	public double getDownloadSpeed() {
		return downloadSpeed;
	}

	@Override
	public String getSections() {
		int numActive = 0;
		synchronized (secList) {
			for(Section sec : secList){
				if(!sec.isClosed())
					numActive++;
			}
		}
		return numActive + "/" + secList.size();
	}

	@Override
	public Calendar getStartDate() {
		Calendar cal = Calendar.getInstance();
		cal.setTimeInMillis(startTime);
		return cal;
	}

	@Override
	public Calendar getCompleteDate() {
		Calendar cal = null;
		if(endTime > 0){
			cal = Calendar.getInstance();
			cal.setTimeInMillis(endTime);
		}
		return cal;
	}

	public String getDirName() {
		return dirName;
	}
}
